package it.unicam.cs.cosy.bioshape.gui;

import it.unicam.cs.cosy.bioshape.gui.UnderlineHighlighter.UnderlineHighlightPainter;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Highlighter;
import javax.swing.text.Highlighter.Highlight;
import javax.swing.text.JTextComponent;
import javax.swing.text.LayeredHighlighter;
import javax.swing.text.Position;
import javax.swing.text.View;

import jsyntaxpane.DefaultSyntaxKit;
import jsyntaxpane.syntaxkits.XmlSyntaxKit;

/**
 * Represents the content of a tab in the simulation, i.e. an customized
 * {@code JEditorPane} for the purpose of the tool
 * 
 * @author Federico Buti
 */
public class DashBoard extends JPanel implements DocumentListener {

	/** Auto-generated serial ID */
	private static final long serialVersionUID = 3028497627651455578L;
	private String title;
	JScrollPane scrPane;
	private ArrayList<String> words;
	private static final String COMMIT_ACTION = "commit";
	private static final String SAVE_ACTION = "save";
	private static final String FORMAT_ACTION = "format";
	private static final String NEW_ACTION = "new";
	private static final String SIMULATION_ACTION = "simulation";
	private final JEditorPane codeEditor;
	Font myFont = new Font("SansSerif", Font.BOLD, 12);
	private static String tag0 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>";
	private static String tag1 = "<simulation xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
	private static String tag2 = "xsi:noNamespaceSchemaLocation=\"../BioShapeInputSchema.xsd\">";
	private static String tag3 = "</simulation>";

	// /**
	// * @return the started
	// */
	// public boolean isStarted() {
	// return started;
	// }

	// /**
	// * @param started
	// * the started to set
	// */
	// public void setStarted(boolean started) {
	// this.started = started;
	// }

	/**
	 * @return the codeEditor
	 */
	public JEditorPane getCodeEditor() {
		return codeEditor;
	}

	private Mode mode;
	private boolean dirtyBit;
	private File file;
	private ClosableTabbedPane parent;
	private Monitor frame;
	private UnderlineHighlightPainter painter;

	/**
	 * @param file
	 *            the file to set
	 */
	public void setFile(File file) {
		this.file = file;
	}

	/**
	 * @return the file
	 */
	public File getFile() {
		return file;
	}

	private static enum Mode {
		INSERT, COMPLETION
	}

	/**
	 * @param file
	 *            file with the XML to load
	 * @param m
	 *            reference to the GUI
	 */
	@SuppressWarnings("synthetic-access")
	public DashBoard(File file, Monitor m) {
		this.setLayout(new BorderLayout());
		this.file = file;
		this.frame = m;
		DefaultSyntaxKit.initKit();
		codeEditor = new JEditorPane();
		scrPane = new JScrollPane(codeEditor);
		scrPane.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
		codeEditor.setContentType("text/xml");
		this.add(scrPane, BorderLayout.CENTER);
		// Configure syntax coloring
		XmlSyntaxKit kit = (XmlSyntaxKit) codeEditor.getEditorKit();
		kit.setProperty("Style.IDENTIFIER", "0xa70c05, 0");
		kit.setProperty("Style.TYPE", "0x0506a7, 0");
		//
		codeEditor.setEditorKit(kit);
		DefaultHighlighter dh = new UnderlineHighlighter(Color.red);
		codeEditor.setHighlighter(dh);
		painter = new UnderlineHighlighter.UnderlineHighlightPainter(Color.red);
		//
		InputMap im = codeEditor.getInputMap();
		ActionMap am = codeEditor.getActionMap();
		im.put(KeyStroke.getKeyStroke("ENTER"), COMMIT_ACTION);
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK
				| InputEvent.SHIFT_DOWN_MASK), FORMAT_ACTION);
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK),
				SAVE_ACTION);
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK),
				NEW_ACTION);
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK),
				SIMULATION_ACTION);
		am.put(COMMIT_ACTION, new CommitAction());
		am.put(FORMAT_ACTION, new FormatAction());
		am.put(SAVE_ACTION, new SaveAction());
		am.put(NEW_ACTION, new NewAction());
		am.put(SIMULATION_ACTION, new SimulationAction());
		//
		initializeVocabulary();
		//
		if (file != null) {
			try {
				// DocumentBuilderFactory dbf = DocumentBuilderFactory
				// .newInstance();
				// DocumentBuilder db = dbf.newDocumentBuilder();
				// Document doc = db.parse(file);
				//
				//
				// TransformerFactory tf = TransformerFactory.newInstance();
				// Transformer transformer = tf.newTransformer();
				// transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,
				// "yes");
				// transformer.setOutputProperty(OutputKeys.INDENT, "yes");
				// StringWriter writer = new StringWriter();
				// transformer.transform(new DOMSource(doc), new StreamResult(
				// writer));
				// String output = writer.getBuffer().toString();
				// codeEditor.setText(output);
				codeEditor.read(new FileReader(file), null);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			} // catch (ParserConfigurationException e) {
				// // TODO Auto-generated catch block
				// e.printStackTrace();
				// } catch (SAXException e) {
				// // TODO Auto-generated catch block
				// e.printStackTrace();
				// } catch (TransformerConfigurationException e) {
				// // TODO Auto-generated catch block
				// e.printStackTrace();
				// } catch (TransformerException e) {
				// // TODO Auto-generated catch block
				// e.printStackTrace();
				// }
		} else
			codeEditor.setText(tag0 + "\n" + tag1 + "\n" + tag2 + "\n\n\n\n"
					+ tag3);
		//
		codeEditor.getDocument().addDocumentListener(this);
	}

	private void initializeVocabulary() {
		// TODO da XSD
		words = new ArrayList<String>(25);
		words.add("activesite");
		words.add("atomicshape");
		words.add("behaviour");
		words.add("compoundshape");
		words.add("coordinates");
		words.add("driver");
		words.add("equilibrium");
		words.add("face");
		words.add("max");
		words.add("min");
		words.add("network");
		words.add("parameter");
		words.add("process");
		words.add("scale");
		words.add("simulation");
		words.add("trans");
		words.add("vertex");
	}

	private class CommitAction extends AbstractAction {
		/** Auto-generated serial ID */
		private static final long serialVersionUID = -318523941430085856L;

		@SuppressWarnings("synthetic-access")
		@Override
		public void actionPerformed(ActionEvent ev) {
			if (mode == Mode.COMPLETION) {
				int pos = codeEditor.getSelectionEnd();
				try {
					codeEditor.getDocument().insertString(pos, " ", null);
				} catch (BadLocationException e) {
					e.printStackTrace();
				}
				codeEditor.setCaretPosition(pos + 1);
				mode = Mode.INSERT;
			} else {
				codeEditor.replaceSelection("\n");
			}
		}
	}

	private class SaveAction extends AbstractAction {
		/** Auto-generated serial ID */
		private static final long serialVersionUID = -3018286597494101640L;

		@SuppressWarnings("synthetic-access")
		@Override
		public void actionPerformed(ActionEvent ev) {
			if (!dirtyBit)
				return;
			frame.doOperations("save");
		}
	}

	private class NewAction extends AbstractAction {
		/** Auto-generated serial ID */
		private static final long serialVersionUID = -3018286597494101640L;

		@SuppressWarnings("synthetic-access")
		@Override
		public void actionPerformed(ActionEvent ev) {
			frame.doOperations("new");
		}
	}

	private class SimulationAction extends AbstractAction {

		/** Auto-generated serial ID */
		private static final long serialVersionUID = -3883926420807691505L;

		@SuppressWarnings("synthetic-access")
		@Override
		public void actionPerformed(ActionEvent ev) {
			if (dirtyBit)
				frame.doOperations("save");
			if (!dirtyBit)
				frame.doOperations("simulation");
		}
	}

	private class FormatAction extends AbstractAction {
		/** Auto-generated serial ID */
		private static final long serialVersionUID = -2264010253797060685L;

		@SuppressWarnings("synthetic-access")
		@Override
		public void actionPerformed(ActionEvent ev) {
			// 1) eat spaces
			Pattern pattern = Pattern.compile("\\s+");
			Matcher matcher = pattern.matcher(codeEditor.getText());
			String t = matcher.replaceAll("");
			pattern = Pattern.compile("(activesite|coordinates|vertex|"
					+ "face|atomicshape|behaviour|compoundshape|trans|process|"
					+ "driver|scale|min|max|parameters|simulation|parameter|"
					+ "equilibrium|driver|network)");
			matcher = pattern.matcher(t);
			// matcher.
			t = matcher.replaceAll("$1 ");
			// 2) put spaces and carriage return
			int opened = 0; // count opened brackets
			boolean wasText = false;
			StringBuilder text = new StringBuilder(t);
			for (int i = 0; i < text.length(); i++) {
				if (text.charAt(i) == '<') {
					if (text.charAt(i + 1) == '/') {
						opened--;
						for (int j = 0; j < opened; j++)
							if (!wasText)
								text.insert(i, "\t");
					} else {
						opened++;
						for (int j = 0; j < opened - 1; j++)
							if (!wasText)
								text.insert(i, "\t");
					}
					if (i - 1 >= 0 && text.charAt(i - 1) != '\n' && !wasText)
						text.insert(i, "\n");
					else
						wasText = false;
					for (; text.charAt(i) != '>'; i++) {
						if (text.charAt(i) == '"' && text.charAt(i - 1) != '='
								&& text.charAt(i + 1) != '/'
								&& text.charAt(i + 1) != '>')
							text.insert(i + 1, " ");
						if (text.charAt(i) == '=' && text.charAt(i - 1) == ' '
								&& text.charAt(i - 2) == 'r')
							text.delete(i - 1, i);
					}
					if (i + 1 != text.length() && text.charAt(i + 1) == '<')
						text.insert(i + 1, "\n");
					if ((text.charAt(i - 1) == '/' || text.charAt(i - 1) == '?')
							&& text.charAt(i) == '>')
						opened--;

				} else if (i != text.length() && text.charAt(i) != '\n') {
					for (int j = 0; j < opened; j++)
						text.insert(i, "");
					// skip
					while (text.charAt(i + 1) != '<') {
						i++;
					}
					wasText = true;
				}
			}
			for (int i = 0; i < text.length(); i++) {
				if (i + 1 != text.length() && text.charAt(i) == ' '
						&& text.charAt(i + 1) == '>')
					text.delete(i, i + 1);
				if (i + 1 != text.length() && text.charAt(i + 2) == 'f'
						&& text.charAt(i + 1) == ' ' && text.charAt(i) == 's')
					text.delete(i + 1, i + 2);
			}
			int index = text.indexOf("XMLSchema-instance\"");
			if (index != -1)
				text.insert(index + 20, "\n");
			index = text.indexOf("<?xmlversion");
			if (index != -1)
				text.insert(index + 5, " ");
			DefaultCaret caret = (DefaultCaret) codeEditor.getCaret();
			caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
			codeEditor.setText(text.toString());
			caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
		}
	}

	/**
	 * @return the title of the tab
	 */
	public String getTitle() {
		return title;
	}

	/**
	 * @return the dirty bit of the tab
	 */
	public boolean isDirtyBit() {
		return dirtyBit;
	}

	@Override
	public void changedUpdate(DocumentEvent arg0) {
		dirtyBit = true;
		updateTitle(dirtyBit);
	}

	@Override
	public void insertUpdate(DocumentEvent ev) {
		dirtyBit = true;
		updateTitle(dirtyBit);
		if (ev.getLength() != 1) {
			return;
		}

		int pos = ev.getOffset();
		String content = "";
		try {
			content = codeEditor.getText(0, pos + 1);
		} catch (BadLocationException e) {
			e.printStackTrace();
		}
		// Find where the word starts
		int w;
		for (w = pos; w >= 0; w--) {
			if (!Character.isLetter(content.charAt(w))) {
				break;
			}
		}
		if (pos - w < 2) {
			// Too few chars
			return;
		}

		String prefix = content.substring(w + 1).toLowerCase();
		int n = Collections.binarySearch(words, prefix);
		if (n < 0 && -n <= words.size()) {
			String match = words.get(-n - 1);
			if (match.startsWith(prefix)) {
				if (content.charAt(w) != '<')
					return;
				// A completion is found
				String completion = match.substring(pos - w);
				// We cannot modify Document from within notification,
				// so we submit a task that does the change later
				SwingUtilities.invokeLater(new CompletionTask(match,
						completion, pos + 1));
			} else {
				// highlight errors if it is a tag!
				if (content.charAt(w) == '/' && !isHighlighted(w + 1))
					SwingUtilities.invokeLater(new ErroneousTask(w + 1, w + 1
							+ prefix.length()));
				if (content.charAt(w) == '<' && !isHighlighted(w + 1))
					SwingUtilities.invokeLater(new ErroneousTask(w + 1, w + 1
							+ prefix.length()));
			}
		} else {
			// Nothing found
			mode = Mode.INSERT;
		}
	}

	private boolean isHighlighted(int position) {
		for (Highlight h : getCodeEditor().getHighlighter().getHighlights()) {
			if (h.getPainter() instanceof UnderlineHighlighter.UnderlineHighlightPainter) {
				if (h.getStartOffset() == position)
					return true;
			}
		}
		return false;
	}

	private class CompletionTask implements Runnable {
		String completion;
		int position;
		String match;

		CompletionTask(String match, String completion, int position) {
			this.match = match;
			this.completion = completion;
			this.position = position;
		}

		@SuppressWarnings("synthetic-access")
		@Override
		public void run() {
			try {
				codeEditor.getDocument().insertString(position, completion,
						null);
				int diff = match.length() - completion.length();
				removeHighlight(position - diff);
			} catch (BadLocationException e) {
				e.printStackTrace();
			}
			codeEditor.setCaretPosition(position + completion.length());
			codeEditor.moveCaretPosition(position);
			mode = Mode.COMPLETION;
		}

		private void removeHighlight(int offset) {
			for (Highlight h : getCodeEditor().getHighlighter().getHighlights()) {
				if (h.getPainter() instanceof UnderlineHighlighter.UnderlineHighlightPainter) {
					if (h.getStartOffset() == offset)
						getCodeEditor().getHighlighter().removeHighlight(h);
				}
			}
		}
	}

	private class ErroneousTask implements Runnable {
		int start;
		int finish;

		ErroneousTask(int start, int finish) {
			this.start = start;
			this.finish = finish;
		}

		@SuppressWarnings("synthetic-access")
		@Override
		public void run() {
			try {
				codeEditor.getHighlighter()
						.addHighlight(start, finish, painter);
			} catch (BadLocationException e) {
				e.printStackTrace();
			}
			codeEditor.moveCaretPosition(finish);
			mode = Mode.COMPLETION;
		}
	}

	@Override
	public void removeUpdate(DocumentEvent arg0) {
		dirtyBit = true;
		updateTitle(dirtyBit);
	}

	/**
	 * @return the XML in the tab
	 */
	public String getText() {
		return codeEditor.getText();
	}

	/**
	 * Sets the title
	 * 
	 * @param title
	 *            new title for the tab
	 */
	public void setTitle(String title) {
		this.title = title;
	}

	/**
	 * set the dirty bit
	 * 
	 * @param dirtyBit
	 *            {@code true} if the tab text has been modified without saving
	 *            {@code false} otherwise
	 */
	public void setDirtyBit(boolean dirtyBit) {
		this.dirtyBit = dirtyBit;
	}

	/**
	 * Updates the title of the simulation tab: if bold the simulation has been
	 * modified without saving. Othewise, it has been saved.
	 * 
	 * @param flag
	 *            {@code true} if the tab title must be bold (not saved)
	 *            {@code false} otherwise.
	 */
	void updateTitle(boolean flag) {
		if (parent == null)
			parent = (ClosableTabbedPane) this.getParent();
		if (flag)
			parent.getTabComponentAt(parent.getSelectedIndex()).label
					.setFont(myFont);
		else
			parent.getTabComponentAt(parent.getSelectedIndex()).label
					.setFont(UIManager.getDefaults().getFont("TabbedPane.font"));
		// update tooltip, always
		parent.setToolTipTextAt(parent.getSelectedIndex(), title);
	}
}

/**
 * Extends the {@code DefaultHighlighter} with the necessary information to
 * underline the text, if an error occurs.
 * 
 * @author Federico Buti Valentina Scicchitano
 */
class UnderlineHighlighter extends DefaultHighlighter {
	public UnderlineHighlighter(Color c) {
		painter = (c == null ? sharedPainter : new UnderlineHighlightPainter(c));
	}

	// Convenience method to add a highlight with
	// the default painter.
	public Object addHighlight(int p0, int p1) throws BadLocationException {
		return addHighlight(p0, p1, painter);
	}

	@Override
	public void setDrawsLayeredHighlights(boolean newValue) {
		// Illegal if false - we only support layered highlights
		if (newValue == false) {
			throw new IllegalArgumentException(
					"UnderlineHighlighter only draws layered highlights");
		}
		super.setDrawsLayeredHighlights(true);
	}

	// Painter for underlined highlights
	static class UnderlineHighlightPainter extends
			LayeredHighlighter.LayerPainter {
		public UnderlineHighlightPainter(Color c) {
			color = c;
		}

		@Override
		public void paint(Graphics g, int offs0, int offs1, Shape bounds,
				JTextComponent c) {
			// Do nothing: this method will never be called
		}

		@Override
		public Shape paintLayer(Graphics g, int offs0, int offs1, Shape bounds,
				JTextComponent c, View view) {
			g.setColor(color == null ? c.getSelectionColor() : color);

			Rectangle alloc = null;
			if (offs0 == view.getStartOffset() && offs1 == view.getEndOffset()) {
				if (bounds instanceof Rectangle) {
					alloc = (Rectangle) bounds;
				} else {
					alloc = bounds.getBounds();
				}
			} else {
				try {
					Shape shape = view.modelToView(offs0,
							Position.Bias.Forward, offs1,
							Position.Bias.Backward, bounds);
					alloc = (shape instanceof Rectangle) ? (Rectangle) shape
							: shape.getBounds();
				} catch (BadLocationException e) {
					return null;
				}
			}

			FontMetrics fm = c.getFontMetrics(c.getFont());
			int baseline = alloc.y + alloc.height - fm.getDescent() + 1;
			g.drawLine(alloc.x, baseline, alloc.x + alloc.width, baseline);
			g.drawLine(alloc.x, baseline + 1, alloc.x + alloc.width,
					baseline + 1);

			return alloc;
		}

		protected Color color; // The color for the underline
	}

	// Shared painter used for default highlighting
	protected static final Highlighter.HighlightPainter sharedPainter = new UnderlineHighlightPainter(
			null);

	// Painter used for this highlighter
	protected Highlighter.HighlightPainter painter;
}